from __future__ import absolute_import

import os
import zlib
import json
import click
import logging
import six

from datetime import datetime
from subprocess import Popen, PIPE, check_output
from six.moves.urllib.parse import urlparse

HERE = os.path.abspath(os.path.dirname(__file__))
SENTRY_CONFIG = os.environ['SENTRY_CONF'] = os.path.join(HERE, 'sentry.conf.py')
os.environ['SENTRY_SKIP_BACKEND_VALIDATION'] = '1'

# No sentry or django imports before this point
from sentry.runner import configure
configure()
from django.conf import settings

# Fair game from here
from django.core.management import call_command

from sentry.utils.apidocs import (
    Runner, MockUtils, iter_scenarios,
    iter_endpoints, get_sections
)
from sentry.web.helpers import render_to_string


OUTPUT_PATH = os.path.join(HERE, 'cache')
HOST = urlparse(settings.SENTRY_OPTIONS['system.url-prefix']).netloc


# We don't care about you, go away
_logger = logging.getLogger('sentry.events')
_logger.disabled = True


def color_for_string(s):
    colors = ('red', 'green', 'yellow', 'blue', 'cyan', 'magenta')
    return colors[zlib.crc32(s) % len(colors)]


def report(category, message, fg=None):
    if fg is None:
        fg = color_for_string(category)
    click.echo('[%s] %s: %s' % (
        six.text_type(datetime.utcnow()).split('.')[0],
        click.style(category, fg=fg),
        message
    ))


def launch_redis():
    report('redis', 'Launching redis server')
    cl = Popen(['redis-server', '-'], stdin=PIPE, stdout=open(os.devnull, 'r+'))
    cl.stdin.write('''
    port %(port)s
    databases %(databases)d
    save ""
    ''' % {
        'port': six.text_type(settings.SENTRY_APIDOCS_REDIS_PORT),
        'databases': 4,
    })
    cl.stdin.flush()
    cl.stdin.close()
    return cl


def spawn_sentry():
    report('sentry', 'Launching sentry server')
    cl = Popen(['sentry', '--config=' + SENTRY_CONFIG, 'run', 'web',
                '-w', '1', '--bind', '127.0.0.1:%s' % settings.SENTRY_APIDOCS_WEB_PORT])
    return cl


def init_db():
    drop_db()
    report('db', 'Migrating database (this can take some time)')
    call_command('syncdb', migrate=True, interactive=False,
                 traceback=True, verbosity=0)


def drop_db():
    report('db', 'Dropping database')
    config = settings.DATABASES['default']
    check_output([
        'dropdb', '-U', config['USER'], '-h', config['HOST'], config['NAME']
    ])
    check_output([
        'createdb', '-U', config['USER'], '-h', config['HOST'], config['NAME']
    ])


class SentryBox(object):

    def __init__(self):
        self.redis = None
        self.sentry = None
        self.task_runner = None

    def __enter__(self):
        self.redis = launch_redis()
        self.sentry = spawn_sentry()
        init_db()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        if self.sentry is not None:
            report('sentry', 'Shutting down sentry server')
            self.sentry.kill()
            self.sentry.wait()
        if self.redis is not None:
            report('redis', 'Stopping redis server')
            self.redis.kill()
            self.redis.wait()


def run_scenario(vars, scenario_ident, func):
    runner = Runner(scenario_ident, func, **vars)
    report('scenario', 'Running scenario "%s"' % scenario_ident)
    func(runner)
    return runner.to_json()


@click.command()
@click.option('--output-path', type=click.Path())
@click.option('--output-format', type=click.Choice(['json', 'markdown', 'both']), default='both')
def cli(output_path, output_format):
    """API docs dummy generator."""
    global OUTPUT_PATH
    if output_path is not None:
        OUTPUT_PATH = os.path.abspath(output_path)
    with SentryBox():
        utils = MockUtils()
        report('org', 'Creating user and organization')
        user = utils.create_user('john@interstellar.invalid')
        org = utils.create_org('The Interstellar Jurisdiction',
                               owner=user)
        report('auth', 'Creating api token')
        api_token = utils.create_api_token(user)

        report('org', 'Creating team')
        team = utils.create_team('Powerful Abolitionist',
                                 org=org)
        utils.join_team(team, user)

        projects = []
        for project_name in 'Pump Station', 'Prime Mover':
            report('project', 'Creating project "%s"' % project_name)
            project = utils.create_project(project_name, teams=[team], org=org)
            release = utils.create_release(project=project, user=user)
            report('event', 'Creating event for "%s"' % project_name)

            event1 = utils.create_event(project=project, release=release,
                                        platform='python')
            event2 = utils.create_event(project=project, release=release,
                                        platform='java')
            projects.append({
                'project': project,
                'release': release,
                'events': [event1, event2],
            })

        vars = {
            'org': org,
            'me': user,
            'api_token': api_token,
            'teams': [{
                'team': team,
                'projects': projects,
            }],
        }

        scenario_map = {}
        report('docs', 'Collecting scenarios')
        for scenario_ident, func in iter_scenarios():
            scenario = run_scenario(vars, scenario_ident, func)
            scenario_map[scenario_ident] = scenario

        section_mapping = {}
        report('docs', 'Collecting endpoint documentation')
        for endpoint in iter_endpoints():
            report('endpoint', 'Collecting docs for "%s"' %
                   endpoint['endpoint_name'])

            section_mapping \
                .setdefault(endpoint['section'], []) \
                .append(endpoint)
        sections = get_sections()

        if output_format in ('json', 'both'):
            output_json(sections, scenario_map, section_mapping)
        if output_format in ('markdown', 'both'):
            output_markdown(sections, scenario_map, section_mapping)


def output_json(sections, scenarios, section_mapping):
    report('docs', 'Generating JSON documents')

    for id, scenario in scenarios.items():
        dump_json('scenarios/%s.json' % id, scenario)

    section_listings = {}
    for section, title in sections.items():
        entries = {}
        for endpoint in section_mapping.get(section, []):
            entries[endpoint['endpoint_name']] = endpoint['title']
            dump_json('endpoints/%s.json' % endpoint['endpoint_name'],
                      endpoint)

        section_listings[section] = {
            'title': title,
            'entries': entries
        }
    dump_json('sections.json', {'sections': section_listings})


def output_markdown(sections, scenarios, section_mapping):
    report('docs', 'Generating markdown documents')
    for section, title in sections.items():
        i = 0
        links = []
        for endpoint in section_mapping.get(section, []):
            i += 1
            path = u"{}/{}.md".format(section, endpoint['endpoint_name'])
            auth = ''
            if len(endpoint['params'].get('auth', [])):
                auth = endpoint['params']['auth'][0]['description']
            payload = dict(
                title=endpoint['title'],
                sidebar_order=i,
                description='\n'.join(endpoint['text']).strip(),
                warning=endpoint['warning'],
                method=endpoint['method'],
                api_path=endpoint['path'],
                query_parameters=endpoint['params'].get('query'),
                path_parameters=endpoint['params'].get('path'),
                parameters=endpoint['params'].get('param'),
                authentication=auth,
                example_request=format_request(endpoint, scenarios),
                example_response=format_response(endpoint, scenarios)
            )
            dump_markdown(path, payload)

            links.append({'title': endpoint['title'], 'path': path})
        dump_index_markdown(section, title, links)


def dump_json(path, data):
    path = os.path.join(OUTPUT_PATH, 'json', path)
    try:
        os.makedirs(os.path.dirname(path))
    except OSError:
        pass
    with open(path, 'w') as f:
        for line in json.dumps(data, indent=2, sort_keys=True).splitlines():
            f.write(line.rstrip() + '\n')


def dump_index_markdown(section, title, links):
    path = os.path.join(OUTPUT_PATH, 'markdown', section, 'index.md')
    try:
        os.makedirs(os.path.dirname(path))
    except OSError:
        pass
    with open(path, 'w') as f:
        contents = render_to_string(
            'sentry/apidocs/index.md',
            dict(title=title, links=links))
        f.write(contents)


def dump_markdown(path, data):
    path = os.path.join(OUTPUT_PATH, 'markdown', path)
    try:
        os.makedirs(os.path.dirname(path))
    except OSError:
        pass
    with open(path, 'w') as f:
        template = u"""---
# This file is automatically generated from the API using `sentry/api-docs/generator.py.`
# Do not manually edit this file.
{}
---
"""
        contents = template.format(json.dumps(data, sort_keys=True, indent=2))
        f.write(contents)


def find_first_scenario(endpoint, scenario_map):
    for scene in endpoint['scenarios']:
        if scene not in scenario_map:
            continue
        try:
            return scenario_map[scene]['requests'][0]
        except IndexError:
            return None
    return None


def format_request(endpoint, scenario_map):
    scene = find_first_scenario(endpoint, scenario_map)
    if not scene:
        return ''
    request = scene['request']
    lines = [
        u"{} {} HTTP/1.1".format(request['method'], request['path']),
        'Host: sentry.io',
        'Authorization: Bearer <token>',
    ]
    lines.extend(format_headers(request['headers']))
    if request['data']:
        lines.append('')
        lines.append(json.dumps(request['data'],
                                sort_keys=True,
                                indent=2))
    return "\n".join(lines)


def format_response(endpoint, scenario_map):
    scene = find_first_scenario(endpoint, scenario_map)
    if not scene:
        return ''
    response = scene['response']
    lines = [
        u"HTTP/1.1 {} {}".format(response['status'], response['reason']),
    ]
    lines.extend(format_headers(response['headers']))
    if response['data']:
        lines.append('')
        lines.append(json.dumps(response['data'],
                                sort_keys=True,
                                indent=2))
    return "\n".join(lines)


def format_headers(headers):
    """Format headers into a list."""
    return [
        u'{}: {}'.format(key, value)
        for key, value
        in headers.items()
    ]


if __name__ == '__main__':
    cli()
